package code

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"reflect"
	"strconv"
	"strings"
)

// ExtractComponents converts ast file into code components model
func ExtractComponents(f *ast.File) File {
	var file File
	file.PackageName = f.Name.Name

	for _, decl := range f.Decls {
		switch t := decl.(type) {
		case *ast.GenDecl:
			extraGen(t, &file)
		case *ast.FuncDecl:
			extraMethod(t, &file)
		}
	}
	return file
}

func extraGen(genDecl *ast.GenDecl, file *File) {
	for _, spec := range genDecl.Specs {
		switch spec := spec.(type) {
		case *ast.ImportSpec:
			var imp Import
			if spec.Name != nil {
				imp.Name = spec.Name.Name
			}
			importPath, err := strconv.Unquote(spec.Path.Value)
			if err != nil {
				fmt.Printf("cannot unquote import %s : %s \n", spec.Path.Value, err)
				continue
			}
			imp.Path = importPath

			file.Imports = append(file.Imports, imp)

		case *ast.TypeSpec:
			switch t := spec.Type.(type) {
			case *ast.StructType:
				file.Structs = append(file.Structs, extractStructType(spec.Name.Name, t))
			case *ast.InterfaceType:
				file.Interfaces = append(file.Interfaces, extractInterfaceType(spec.Name.Name, t))
			}
		}
	}
}

func extraMethod(funcDecl *ast.FuncDecl, file *File) {
	recv := funcDecl.Recv
	name := funcDecl.Name
	funcType := funcDecl.Type
	method := extractFunction(name.Name, nil, funcType)
	if recv != nil && len(recv.List) > 0 {
		method.Receiver = getType(recv.List[0].Type)
	}
	file.Methods = append(file.Methods, method)
}

func ExportFile(file *File) (*File, error) {
	structs := make([]Struct, len(file.Structs))
	for i, s := range file.Structs {
		ns, err := exportStruct(s, file.PackageName)
		if err != nil {
			return nil, err
		}
		structs[i] = *ns
	}
	interfaces := make([]InterfaceType, len(file.Interfaces))
	for i, iterf := range file.Interfaces {
		ni, err := toExternalType(iterf, file.PackageName)
		if err != nil {
			return nil, err
		}
		interfaces[i] = ni.(InterfaceType)
	}
	f := File{
		PackageName: file.PackageName,
		Imports:     file.Imports,
		Structs:     structs,
		Interfaces:  interfaces,
	}
	return &f, nil
}

func exportStruct(s Struct, packageAlias string) (*Struct, error) {
	fields := make([]StructField, len(s.Fields))
	for i, f := range s.Fields {
		extType, err := toExternalType(f.Type, packageAlias)
		if err != nil {
			return nil, err
		}
		fields[i] = StructField{
			Name: f.Name,
			Type: extType,
			Tags: f.Tags,
		}
	}
	str := Struct{
		PackageAlias: packageAlias,
		Name:         s.Name,
		Fields:       fields,
	}
	return &str, nil
}

func toExternalType(p Type, packageAlias string) (Type, error) {
	switch t := p.(type) {
	case ExternalType:
		return ExternalType{Name: t.Name, PackageAlias: t.PackageAlias}, nil
	case SimpleType:
		if t.IsBuildIn() {
			return t, nil
		}
		return ExternalType{Name: t.Code(), PackageAlias: packageAlias}, nil
	case ArrayType:
		containedType, err := toExternalType(t.ContainedType, packageAlias)
		if err != nil {
			return nil, err
		}
		return ArrayType{ContainedType: containedType}, nil
	case PointerType:
		containedType, err := toExternalType(t.ContainedType, packageAlias)
		if err != nil {
			return nil, err
		}
		return PointerType{ContainedType: containedType}, nil
	case MapType:
		valueType, err := toExternalType(t.ValueType, packageAlias)
		if err != nil {
			return nil, err
		}
		keyType, err := toExternalType(t.KeyType, packageAlias)
		if err != nil {
			return nil, err
		}
		return MapType{keyType, valueType}, nil
	case InterfaceType:
		methods := make([]Method, len(t.Methods))
		for i, m := range t.Methods {
			params := make([]Param, len(m.Params))
			for j, pa := range m.Params {
				paramType, err := toExternalType(pa.Type, packageAlias)
				if err != nil {
					return nil, err
				}
				param := Param{Name: pa.Name, Type: paramType}
				params[j] = param
			}
			returns := make([]Type, len(m.Returns))
			for j, re := range m.Returns {
				retType, err := toExternalType(re, packageAlias)
				if err != nil {
					return nil, err
				}
				returns[j] = retType
			}
			methods[i] = Method{Name: m.Name, Params: params, Returns: returns, Comments: m.Comments}
		}
		return InterfaceType{Name: t.Name, Methods: methods}, nil
	default:
		return nil, fmt.Errorf("unSupported type to convert to External type")
	}
}

func extractStructType(name string, structType *ast.StructType) Struct {
	str := Struct{
		Name: name,
	}

	for _, field := range structType.Fields.List {
		var strField StructField
		for _, name := range field.Names {
			strField.Name = name.Name
			break
		}
		strField.Type = getType(field.Type)
		if field.Tag != nil {
			strField.Tags = extractStructTag(field.Tag.Value)
		}

		str.Fields = append(str.Fields, strField)
	}

	return str
}

func extractInterfaceType(name string, interfaceType *ast.InterfaceType) InterfaceType {
	intf := InterfaceType{
		Name: name,
	}

	for _, method := range interfaceType.Methods.List {
		funcType, ok := method.Type.(*ast.FuncType)
		if !ok {
			continue
		}

		var name string
		for _, n := range method.Names {
			name = n.Name
			break
		}

		var comments []string
		if method.Doc != nil {
			for _, comment := range method.Doc.List {
				commentRunes := []rune(comment.Text)
				commentText := strings.TrimSpace(string(commentRunes[2:]))
				comments = append(comments, commentText)
			}
		}

		meth := extractFunction(name, comments, funcType)

		intf.Methods = append(intf.Methods, meth)
	}

	return intf
}

var tagKeys = []string{"mapper"}

func extractStructTag(tagValue string) map[string][]string {
	tagValue = strings.Trim(tagValue, "`")
	st := reflect.StructTag(tagValue)

	tags := make(map[string][]string)
	for _, tagKey := range tagKeys {
		if value, ok := st.Lookup(tagKey); ok {
			tagValues := strings.Split(value, ",")
			tags[tagKey] = tagValues
		}
	}

	return tags
}

func extractFunction(name string, comments []string, funcType *ast.FuncType) Method {
	meth := Method{
		Name:     name,
		Comments: comments,
	}
	for _, param := range funcType.Params.List {
		paramType := getType(param.Type)

		if len(param.Names) == 0 {
			meth.Params = append(meth.Params, Param{Type: paramType})
			continue
		}

		for _, name := range param.Names {
			meth.Params = append(meth.Params, Param{
				Name: name.Name,
				Type: paramType,
			})
		}
	}

	if funcType.Results != nil {
		for _, result := range funcType.Results.List {
			meth.Returns = append(meth.Returns, getType(result.Type))
		}
	}

	return meth
}

func getType(expr ast.Expr) Type {
	switch expr := expr.(type) {
	case *ast.Ident:
		return SimpleType(expr.Name)

	case *ast.SelectorExpr:
		xExpr, ok := expr.X.(*ast.Ident)
		if !ok {
			return ExternalType{Name: expr.Sel.Name}
		}
		return ExternalType{PackageAlias: xExpr.Name, Name: expr.Sel.Name}

	case *ast.StarExpr:
		containedType := getType(expr.X)
		return PointerType{ContainedType: containedType}

	case *ast.ArrayType:
		containedType := getType(expr.Elt)
		return ArrayType{ContainedType: containedType}

	case *ast.MapType:
		keyType := getType(expr.Key)
		valueType := getType(expr.Value)
		return MapType{KeyType: keyType, ValueType: valueType}

	case *ast.InterfaceType:
		return extractInterfaceType("", expr)
	}

	return nil
}

func ExtractComponentsByPath(componentPath string) (*File, error) {
	filesSpec := File{
		PackageName: "",
		Imports:     make([]Import, 0),
		Structs:     make([]Struct, 0),
		Interfaces:  make([]InterfaceType, 0),
	}

	fset := token.NewFileSet()
	ps, err := parser.ParseDir(fset, componentPath, nil, parser.ParseComments)
	if err != nil {
		return nil, err
	}
	for pName, pkg := range ps {
		if strings.HasSuffix(pName, "_test") || pName == "main" {
			continue
		}
		filesSpec.PackageName = pName
		for _, fi := range pkg.Files {
			f := ExtractComponents(fi)
			filesSpec.Imports = append(filesSpec.Imports, f.Imports...)
			filesSpec.Structs = append(filesSpec.Structs, f.Structs...)
			filesSpec.Interfaces = append(filesSpec.Interfaces, f.Interfaces...)
			filesSpec.Methods = append(filesSpec.Methods, f.Methods...)
		}
	}
	return &filesSpec, nil
}
